常用十大经典排序算法总结(动图演示,绝对值得收藏)

心路历程: 排序算法可以算是任何编程语言数据结构和底层源码算法的基础。之前无数次接触过,始终没有整理归纳过,最近整理分享如下。希望大家在浏览的过程中,都能有所收获;此外在某些见识方面有所欠缺的地方,烦请大佬们指正,共同进步,不胜感激…

本篇博客所分享的知识非常硬核,建议各位看官(尤其是刚学编程的道友们),赶紧搬好小板凳,带好西瓜,我们边看边吃瓜。
在这里插入图片描述

说错了,是带着小本,边看边记下来
在这里插入图片描述

申明 本篇博客是站在先前大佬的肩膀上总结整理的,不足之处,请指点,谢谢!

算法简介

算法分类

我们通常所说的排序算法往往指的是内部排序算法,即数据记录在内存中进行排序。

排序算法大体可分为两种:

一种是比较排序,时间复杂度O(nlogn) ~ O(n^2),主要有:冒泡排序,选择排序,插入排序,希尔排序,归并排序,堆排序,快速排序等。

另一种是非比较排序,时间复杂度可以达到O(n),主要有:计数排序,基数排序,桶排序等。

算法性能对照表

在这里插入图片描述

相关概念

  • 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
  • 不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
  • 时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
  • 空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数

排序算法的稳定性是指排序前后两个相等的数的相对顺序不变。更重要的是排序算法的稳定性是相对的,不是绝对的。排序算法是否为稳定的是由具体算法决定的,不稳定的算法在某种条件下可以变为稳定的算法,而稳定的算法在某种条件下也可以变为不稳定的算法。比如说冒泡排序把交换条件更改为a[i]>=a[i+1]就是不稳定的排序,a[i]>a[i+1]就是稳定的排序。

排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,前一个键排序的结果可以为后一个键排序所用。

个人理解:排序算法的核心趋势必将是用内存缓存去换磁盘读取,用空间去换时间,旨在优化创新。排序算法大多数情况下是交互使用的,各有千秋,如何交互,什么情况下使用何种算法,是优化的核心所在。

比较排序

冒泡排序(Bubble Sort)

冒泡排序是一种极其简单的排序算法,几乎是所有编程人员学习的第一个排序算法。它重复地走访过要排序的元素,依次比较相邻两个元素,如果他们的顺序错误就把他们调换过来,直到没有元素再需要交换,排序完成。这个算法的名字由来是因为越小(或越大)的元素会经由交换慢慢“浮”到数列的顶端。

1. 算法描述

  1. 比较相邻的元素,如果前一个比后一个大,就交换它们两个的值。
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素就是这一轮最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 重复上述所有步骤,直到没有任何可交换的两个元素为止,排序结束。

2. 算法性能

  • 分类 -------------- 内部比较排序
  • 数据结构 ---------- 数组
  • 最差时间复杂度 ---- O(n^2)
  • 最优时间复杂度 ---- 如果能在内部循环第一次运行时,使用一个旗标来表示有无需要交换的可能,可以把最优时间复杂度降低到O(n)
  • 平均时间复杂度 ---- O(n^2)
  • 所需辅助空间 ------ O(1)
  • 稳定性 ------------ 稳定

3. 动图演示

在这里插入图片描述

4. 代码实现

public class Tools {
    //交换两个数的值
    public static void exchangeValue(int[] nums,int a,int b){
        int temp;
        temp = nums[a];
        nums[a] = nums[b];
        nums[b] = temp;
    }
}

//冒泡排序
public  static void bubbleSort(int nums[]){
    // 每次最大元素就像气泡一样"浮"到数组的最后
    for (int i = 0; i < nums.length-1; i++) {
        // 依次比较相邻的两个元素,使较大的那个向后移
        for (int j = 0; j < nums.length-1-i; j++) {
            // 如果条件改成nums[j]>=nums[j+1],则变为不稳定的排序算法
            if (nums[j]>nums[j+1]){
                Tools.exchangeValue(nums,j,j+1);
            }
        }
    }
}

尽管冒泡排序是最容易了解和实现的排序算法之一,但它对于少数元素之外的数列排序是很没有效率的。

冒泡排序的改进:鸡尾酒排序

鸡尾酒排序,也叫定向冒泡排序,是冒泡排序的一种改进。此算法与冒泡排序的不同处在于从低到高然后从高到低,而冒泡排序则仅从低到高去比较序列里的每个元素。他可以得到比冒泡排序稍微好一点的效能。

1. 算法描述

  1. 比较相邻的元素,如果前一个比后一个大,就交换它们两个的值。
  2. 对每一对相邻元素作步骤1,从左向右开始第一对到结尾的最后一对。这步做完后,最后的元素就是这一轮最大的数。
  3. 对每一对相邻元素作步骤1,除了最后一个,从右向左开始第一对到结尾的最后一对。这步做完后,最前面的元素就是这一轮最小的数
  4. 针对所有的元素重复以上的步骤,除了最后一个和最前面的一个。
  5. 重复上述所有步骤,直到没有任何可交换的两个元素为止,排序结束。

2. 算法性能

  • 分类 -------------- 内部比较排序
  • 数据结构 ---------- 数组
  • 最差时间复杂度 ---- O(n^2)
  • 最优时间复杂度 ---- 如果序列在一开始已经大部分排序过的话,会接近O(n)
  • 平均时间复杂度 ---- O(n^2)
  • 所需辅助空间 ------ O(1)
  • 稳定性 ------------ 稳定

3. 动图演示

在这里插入图片描述

4. 代码实现

public class Tools {
    //交换两个数的值
    public static void exchangeValue(int[] nums,int a,int b){
        int temp;
        temp = nums[a];
        nums[a] = nums[b];
        nums[b] = temp;
    }
}

public  static void cocktailSort(int nums[]){
    //初始化边界值
    int left = 0, right = nums.length-1;
    for (int i = 0; i < nums.length-1; i++) {
        // 前半轮,将最大元素放到后面
        for (int j = left; j < right; j++) {
            if (nums[j]>nums[j+1]){
                Tools.exchangeValue(nums,j,j+1);
            }
        }
        right--;
        // 后半轮,将最小元素放到前面
        for (int j = right; j >left ; j--) {
            if (nums[j]<nums[j-1]){
                Tools.exchangeValue(nums,j,j-1);
            }
        }
        left++;
    }
}

鸡尾酒排序对冒泡排序的改进并不是绝对的,只是在部分相对有序的序列中,性能会比冒泡排序好很多。比如以序列(3,8,41,59,1)为例,鸡尾酒排序只需要访问一次序列就可以完成排序,但如果使用冒泡排序则需要四次。但是在乱数序列的状态下,鸡尾酒排序与冒泡排序的效率都很差劲。

选择排序(Selection Sort)

选择排序也是一种简单直观的排序算法。它的工作原理很容易理解:初始时在序列中找到最小(大)元素,放到序列的起始位置(末尾位置)作为已排序序列;然后,再从剩余未排序元素中继续寻找最小(大)元素,放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

1. 算法描述

  1. 假定起始位置的元素为每轮的最小值min,下标为起始位置的下标。
  2. 从前到后依次遍历序列中的元素,每个元素都和这个最小值进行比较,如果该元素的值小于这个最小值,那么就把这个元素的下标赋给最小值min;每轮结束,最终这个最小值min就是实际最小值min。
  3. 判断实际最小值min的下标和假定最小值的下标,也就是每轮的起始位置是否一致,不一致则交换下标所指的两个元素。
  4. 针对所有的元素重复以上的步骤,除了最前面一个。
  5. 重复上述所有步骤,直到没有任何可和最小值min比较的元素为止,排序结束。

2. 算法性能

  • 分类 -------------- 内部比较排序
  • 数据结构 ---------- 数组
  • 最差时间复杂度 ---- O(n^2)
  • 最优时间复杂度 ---- O(n^2)
  • 平均时间复杂度 ---- O(n^2)
  • 所需辅助空间 ------ O(1)
  • 稳定性 ------------ 不稳定

3. 动图演示
在这里插入图片描述

4. 代码实现

public class Tools {
    //交换两个数的值
    public static void exchangeValue(int[] nums,int a,int b){
        int temp;
        temp = nums[a];
        nums[a] = nums[b];
        nums[b] = temp;
    }
}

 //选择排序
public  static void selectionSort(int nums[]){
    // i为已排序序列的末尾
    for (int i = 0; i < nums.length-1; i++) {
        int min = i;
        // 未排序序列
        for (int j = i+1; j <= nums.length-1; j++) {
            // 找出未排序序列中的最小值
            if (nums[min]>nums[j]){
                min = j;
            }
        }
        // 交换假定最小值和每轮实际最小值所代表的元素
        //放到已排序序列的末尾,该操作很有可能把稳定性打乱,所以选择排序是不稳定的排序算法
        if (min!=i){
            Tools.exchangeValue(nums,i,min);
        }
    }
}

选择排序每遍历一次都记住了当前最小(大)元素的位置,相当于每一轮都是再选择该轮里的最小(大)元素,最后仅需一次交换操作即可将其放到合适的位置。

选择排序和冒泡排序的效率几乎一致,同样只适合数据量少的情况下的排序。

选择排序是不稳定的排序算法,不稳定发生在每轮仅有的一次元素交换的时候。 比如{ 6, 18, 6,3, 9 },一次选择的最小元素是3,然后把3和第一个6进行交换,从而改变了两个元素6的相对次序。

插入排序(Insertion Sort)

插入排序是一种简单直观的排序算法。它的工作原理非常类似于我们抓扑克牌,对于未排序数据(右手抓到的牌),在已排序序列(左手已经排好序的手牌)中从后向前扫描,找到相应位置并插入。

1. 算法描述

  1. 从第一个元素开始,该元素可以认为已经被排序;
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置;
  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
  5. 将新元素插入到该位置后;
  6. 重复步骤2~5。

2. 算法性能

  • 分类 ------------- 内部比较排序
  • 数据结构 ---------- 数组
  • 最差时间复杂度 ---- 最坏情况为输入序列是降序排列的,此时时间复杂度O(n^2)
  • 最优时间复杂度 ---- 最好情况为输入序列是升序排列的,此时时间复杂度O(n)
  • 平均时间复杂度 ---- O(n^2)
  • 所需辅助空间 ------ O(1)
  • 稳定性 ------------ 稳定

3. 动图演示
在这里插入图片描述
4. 代码实现

public  static void insertionSort(int nums[]){
    for (int i = 1; i < nums.length; i++) {   // 类似抓扑克牌排序
           int temp = nums [i];      // 右手抓到一张扑克牌
           int j = i-1;             // 拿在左手上的牌总是排序好的
            while (j>=0 && nums[j]>temp){   // 将抓到的牌与手牌从右向左进行比较
                nums[j+1] = nums[j];     // 如果该手牌比抓到的牌大,就将其右移
                j--;
            }
            nums[j+1] = temp;    // 直到该手牌比抓到的牌小(或二者相等),将抓到的牌插入到该手牌右边(相等元素的相对次序未变,所以插入排序是稳定的)
    }
}

插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

插入排序不适合对于数据量比较大的排序应用。比如说java ArrayList源码中当size() <47 时就采用的就是插入排序。

插入排序的改进:二分插入排序

对于插入排序,如果比较操作的代价比交换操作大的话,可以采用二分查找法来减少比较操作的次数,我们称为二分插入排序。这里所用到的二分是对已经排好序的序列进行二分快速查找新元素的位置。

1. 算法描述

  1. 从第一个元素开始,该元素可以认为已经被排序;
  2. 取出下一个元素,在已经排序的元素序列中进行二分法从后向前扫描;
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置;
  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
  5. 将新元素插入到该位置后;
  6. 重复步骤2~5。

2. 算法性能

  • 分类 -------------- 内部比较排序
  • 数据结构 ---------- 数组
  • 最差时间复杂度 ---- O(n^2)
  • 最优时间复杂度 ---- O(nlogn)
  • 平均时间复杂度 ---- O(n^2)
  • 所需辅助空间 ------ O(1)
  • 稳定性 ------------ 稳定

3. 动图演示

如下图所示,当插入最后一个元素 12 的过程如下:
在这里插入图片描述

4. 代码实现

public static void insertionSortDichotomy(int[] nums) {
    for (int i = 1; i < nums.length; i++) {
        int get = nums[i];                    // 右手抓到一张扑克牌
        int left = 0;                    // 拿在左手上的牌总是排序好的,所以可以用二分法
        int right = i - 1;                // 手牌左右边界进行初始化
        while (left <= right){           // 采用二分法定位新牌的位置
            int mid = (left + right) / 2;
            if (nums[mid] > get)
                right = mid - 1;
            else
                left = mid + 1;
        }
        for (int j = i - 1; j >= left; j--){    // 将欲插入新牌位置右边的牌整体向右移动一个单位
            nums[j + 1] = nums[j];
        }
        nums[left] = get;                    // 将抓到的牌插入手牌
    }
}

当n较大时,二分插入排序的比较次数比直接插入排序的最差情况好得多,但比直接插入排序的最好情况要差,所当以元素初始序列已经接近升序时,直接插入排序比二分插入排序比较次数少。二分插入排序元素移动次数与直接插入排序相同,依赖于元素初始序列。

希尔排序(Shell Sort)

希尔排序,也叫递减增量排序,是插入排序的一种更高效的改进版本。希尔排序是不稳定的排序算法,虽然一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱。

1. 算法描述

  1. 先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序。
  2. 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;比如下面我选择的序列就是1,3,7,…,2*h+1(h代表前一个增量t k-1)。
  3. 按增量序列个数k,对序列进行k 趟排序;
  4. 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子序列进行直接插入排序。当增量因子为0 时,排序也就完成了。

2. 算法性能

  • 分类 -------------- 内部比较排序
  • 数据结构 ---------- 数组
  • 最差时间复杂度 ---- 根据步长序列的不同而不同。已知最好的为O(n(logn)^2)
  • 最优时间复杂度 ---- O(n)
  • 平均时间复杂度 ---- 根据步长序列的不同而不同。
  • 所需辅助空间 ------ O(1)
  • 稳定性 ------------ 不稳定

3. 动图展示
在这里插入图片描述
是不是看不过来,没关系,看下图再脑补一下就差不多了:

在这里插入图片描述

4. 代码实现

public static void shellSort(int[] nums) {
    int h = 0;
    while (h <= nums.length) {                         // 生成初始增量
        h = 2 * h + 1;
    }
    while (h >= 1) {
        for (int i = h; i < nums.length; i++) {
            int j = i - h;
            int get = nums[i];
            while (j >= 0 && nums[j] > get) {
                nums[j + h] = nums[j];
                j = j - h;
            }
            nums[j + h] = get;
        }
        h = (h - 1) / 2;                    // 递减增量
    }
}

希尔排序通过将比较的全部元素分为几个区域来提升插入排序的性能。这样可以让一个元素可以一次性地朝最终位置前进一大步。比如说上述代码h = 2 * h + 1;就是自定义的增量序列;然后算法再取越来越小的步长h = (h - 1) / 2;进行排序,算法的最后一步就是普通的插入排序。

假设有一个很小的数据在一个已按升序排好序的数组的末端。如果直接插入排序,可能会进行n次的比较和交换才能将该数据移至正确位置。而希尔排序会用较大的步长移动数据,所以小数据只需进行少数比较和交换即可到正确位置。也就是说,希尔排序比插入排序更适用于序列两端数值极限的情况。

归并排序(Merge Sort)

归并排序是创建在归并操作上的一种有效的排序算法,归并排序的实现分为递归实现与非递归(迭代)实现。递归实现是指将已有序的子序列两两合并,合并之后的序列,再进行两两合并,最后得到一个完全有序的序列。非递归实现是指把一个序列分为两个子序列,再将每个子序列分为两个子序列,…,直到最小的子序列数量为2或者1时,进行比较排序每个最小的子序列的序列,然后再将每个子序列两两合并,合并之后的序列,再进行两两合并,直到合并成一个序列。

1. 算法描述

  1. 把长度为n的序列相对均匀的分成两个长度为n/2的子序列(n为基数时,分为n/2和n/2+1)。
  2. 对这两个子序列分别采用归并排序;归并操作指的是将两个已经排序的序列合并成一个序列的操作。
  3. 将两个排序好的子序列合并成一个最终的排序序列。

2. 算法性能

  • 分类 -------------- 内部比较排序
  • 数据结构 ---------- 数组
  • 最差时间复杂度 ---- O(nlogn)
  • 最优时间复杂度 ---- O(nlogn)
  • 平均时间复杂度 ---- O(nlogn)
  • 所需辅助空间 ------ O(n)
  • 稳定性 ------------ 稳定

3. 动画演示

在这里插入图片描述

4. 代码实现

 // 合并两个已排好序的数组nums[left...mid]和nums[mid+1...right]
private static void merge(int[] nums, int left, int mid, int right){
    int len = right - left + 1;
    int[] temp = new int[len];       // 辅助空间O(n)
    int index = 0;
    int i = left;                   // 前一数组的起始元素
    int j = mid + 1;                // 后一数组的起始元素
    while (i <= mid && j <= right) {
        temp[index++] = nums[i] <= nums[j] ? nums[i++] : nums[j++];  // 带等号保证归并排序的稳定性
    }
    while (i <= mid) {
        temp[index++] = nums[i++];
    }
    while (j <= right) {
        temp[index++] = nums[j++];
    }
    for (int k = 0; k < len; k++) {
        nums[left++] = temp[k];
    }
}

// 递归实现的归并排序(自顶向下)
public static void MergeSortRecursion(int nums[], int left, int right) {
	// 当待排序的序列长度为1时,递归开始回溯,进行merge操作
    if (left == right)  return;
    int mid = (left + right) / 2;
    MergeSortRecursion(nums, left, mid);
    MergeSortRecursion(nums, mid + 1, right);
    merge(nums, left, mid, right);
}

// 非递归(迭代)实现的归并排序(自底向上)
public static void MergeSortIteration(int nums[], int len) {
    int left, mid, right;// 子数组索引,前一个为nums[left...mid],后一个子数组为nums[mid+1...right]
    for (int i = 1; i < len; i *= 2) {    // 子数组的大小i初始为1,每轮翻倍
        left = 0;
        // 后一个子数组存在(需要归并)
        while (left + i < len) {
            mid = left + i - 1;
            right = mid + i < len ? mid + i : len - 1;// 后一个子数组大小可能不够
            merge(nums, left, mid, right);
            left = right + 1;               // 前一个子数组下标向后移动
            System.out.println();
        }
    }
}

**归并排序是一种稳定的排序方法。**和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度。代价是需要额外的内存空间。

在java ArrayList 的源码中当ArrayList 具备一点数据结构,并且size()>=286时,采用的就是归并排序。

快速排序(Quick Sort)

快速排序使用分治策略(Divide and Conquer)来来把一个序列分为两个子序列和一个已经确认最终位置的元素(假定中位数),递归依次将子序列分为两个子序列和一个已经确认最终位置的元素,…,直到确定原序列所有元素最终的位置为止,此时排序也就自然而然的完成了。

1. 算法描述

  1. 从序列中挑出一个元素,称为 “基准”(pivot),通常称为假定中位数;
  2. 把所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置,也就是说确定了这个元素在最终结果序列的最终位置。这个称为分区(partition)操作;
  3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子序列排序。

2. 算法性能

  • 分类 ------------ 内部比较排序
  • 数据结构 --------- 数组
  • 最差时间复杂度 ---- 每次选取的基准都是最大(或最小)的元素,导致每次只划分出了一个分区,需要进行n-1次划分才能结束递归,时间复杂度为O(n^2)
  • 最优时间复杂度 ---- 每次选取的基准都是中位数,这样每次都均匀的划分出两个分区,只需要logn次划分就能结束递归,时间复杂度为O(nlogn)
  • 平均时间复杂度 ---- O(nlogn)
  • 所需辅助空间 ------ 主要是递归造成的栈空间的使用(用来保存left和right等局部变量),取决于递归树的深度,一般为O(logn),最差为O(n)
  • 稳定性 ---------- 不稳定

3. 动图展示
在这里插入图片描述

4. 代码实现

public class Tools {
    //交换两个数的值
    public static void exchangeValue(int[] nums,int a,int b){
        int temp;
        temp = nums[a];
        nums[a] = nums[b];
        nums[b] = temp;
    }
}

//快速排序
// 法一:假定中位数,寻找中位数的最终位置
private int partitionMid(int nums[],int left,int right){
    // 这里每次都选择第一个元素作为基准
    int pivot = nums[left],temp = left;
    while(left != right){
        while (left<right && nums[right]>=pivot) right--;
        while (left<right && nums[left]<=pivot)  left++;
        if (right != left) Tools.exchangeValue(nums,right,left);
    }
    if (temp!=left){
        nums[temp] = nums[left];
        nums[left] = pivot;
    }
    return left;
}

// 法二:选定基准,改变其他元素的位置
private int partitionMax(int nums[],int left,int right){
    int pivot = nums[right]; // 这里每次都选择最后一个元素作为基准
    int temp = left - 1;                // temp为小于基准的子数组最后一个元素的索引
    for (int i = left; i <right; i++){  // 遍历基准以外的其他元素
        if (nums[i] <= pivot) {              // 把小于等于基准的元素放到前一个子数组末尾
            Tools.exchangeValue(nums,i,++temp);
        }
    }
    Tools.exchangeValue(nums,right,temp+1);        // 最后把基准放到前一个子数组的后边,剩下的子数组既是大于基准的子数组
    // 该操作很有可能把后面元素的稳定性打乱,所以快速排序是不稳定的排序算法
    return temp+1; // 返回基准的索引
}


//
public void quickSort(int nums[], int left, int right) {
    if (left>=right){
        return;
    }
    int pivot_index = partitionMid(nums, left, right);
    quickSort(nums,left,pivot_index-1);
    quickSort(nums,pivot_index+1,right);
}

快速排序是不稳定的排序算法,不稳定发生在基准元素或者中位数与nums[temp]交换的时刻。

在java ArrayList 的源码中当ArrayList 不具备一点数据结构,或者说size()<286并且size()>47时,采用的就是快速排序。

堆排序(Heap Sort)

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质**:即子结点的键值或索引总是小于(或者大于)它的父节点。**

1. 算法描述

  1. 由输入的无序数组构造一个最大堆,作为初始的无序区
  2. 把堆顶元素(最大值)和堆尾元素互换
  3. 把堆(无序区)的尺寸缩小1,并调用heapify(A, 0)从新的堆顶元素开始进行堆调整
  4. 重复步骤2,直到堆的尺寸为1

2. 算法性能

  • 分类 -------------- 内部比较排序
  • 数据结构 ---------- 数组
  • 最差时间复杂度 ---- O(nlogn)
  • 最优时间复杂度 ---- O(nlogn)
  • 平均时间复杂度 ---- O(nlogn)
  • 所需辅助空间 ------ O(1)
  • 稳定性 ------------ 不稳定

3. 动画演示
在这里插入图片描述

4. 代码实现

public class Tools {
    //交换两个数的值
    public static void exchangeValue(int[] nums,int a,int b){
        int temp;
        temp = nums[a];
        nums[a] = nums[b];
        nums[b] = temp;
    }
}

// 从nums[i]向下进行堆调整
private static void Heapify(int nums[], int i, int size) {
    int left_child = 2 * i + 1;         // 左孩子索引
    int right_child = 2 * i + 2;        // 右孩子索引
    int max = i;                        // 选出当前结点与其左右孩子三者之中的最大值
    if (left_child < size && nums[left_child] > nums[max]) max = left_child;
    if (right_child < size && nums[right_child] > nums[max]) max = right_child;
    if (max != i) {
        Tools.exchangeValue(nums, i, max); // 把当前结点和它的最大(直接)子节点进行交换
        Heapify(nums, max, size);          // 递归调用,继续从当前结点向下进行堆调整
    }
}

// 建堆,时间复杂度O(n)
private static int BuildHeap(int nums[], int n) {
    int heap_size = n;
    // 从每一个非叶结点开始向下进行堆调整
    for (int i = heap_size / 2 - 1; i >= 0; i--){
        Heapify(nums, i, heap_size);
    }
    return heap_size;
}

public static void HeapSort(int nums[], int n) {
    int heap_size = BuildHeap(nums, n);    // 建立一个最大堆
    while (heap_size > 1) {                 // 堆(无序区)元素个数大于1,未完成排序
        // 将堆顶元素与堆的最后一个元素互换,并从堆中去掉最后一个元素
        // 此处交换操作很有可能把后面元素的稳定性打乱,所以堆排序是不稳定的排序算法
        Tools.exchangeValue(nums, 0, --heap_size);
        Heapify(nums, 0, heap_size);     // 从新的堆顶元素开始向下进行堆调整,时间复杂度O(logn)
    }
}

堆排序是不稳定的排序算法,不稳定发生在堆顶元素与nums[i]交换的时刻。

非比较排序

计数排序(Counting Sort)

计数排序用到一个额外的计数数组C,根据数组C来将原数组A中的元素排到正确的位置。

1. 算法描述

  1. 找出待排序的数组中最大和最小的元素;
  2. 统计数组中每个值为i的元素出现的次数,存入数组bucket的第i项;
  3. 对所有的计数累加(从bucket中的第一个元素开始,每一项和前一项相加);
  4. 反向填充目标数组:将每个元素i放在新数组的第bucket(i)项,每放一个元素就将bucket(i)减去1。

2. 算法性能

  • 分类 ------------ 内部非比较排序
  • 数据结构 --------- 数组
  • 最差时间复杂度 ---- O(n + k)
  • 最优时间复杂度 ---- O(n + k)
  • 平均时间复杂度 ---- O(n + k)
  • 所需辅助空间 ------ O(n + k)
  • 稳定性 ----------- 稳定

3. 动图演示
在这里插入图片描述

4. 代码实现

public static int[] CountingSort(int[] nums) {
    if (nums.length == 0) return nums;
    int bias, min = nums[0], max = nums[0];
    //找出待排序的数组中最大和最小的元素
    for (int i = 1; i < nums.length; i++) {
        if (nums[i] > max) max = nums[i];
        if (nums[i] < min) min = nums[i];
    }
    bias =  - min;
    int[] bucket = new int[max - min + 1];
    // 初始化bucket元素为0
    Arrays.fill(bucket, 0);
    //找出待排序的数组元素出现的次数一个一个装进bucket
    for (int i = 0; i < nums.length; i++) {
        bucket[nums[i] + bias]++;
    }
    int index = 0, i = 0;
    //反向填充目标数组:将每个元素i放在新数组的第bucket(i)项,每放一个元素就将bucket(i)减去1
    while (index < nums.length) {
        if (bucket[i] != 0) {
            nums[index] = i - bias;
            bucket[i]--;
            index++;
        } else
            i++;
    }
    return nums;
}

计数排序的时间复杂度和空间复杂度与数组nums的数据范围(nums中元素的最大值与最小值的差加上1)有关,因此对于数据范围很大的数组,计数排序需要大量时间和内存。

例如:对0到99之间的数字进行排序,其排序速度快于任何比较排序算法。计数排序是最好的算法,然而计数排序并不适合按字母顺序排序人名,将计数排序用在基数排序算法中,能够更有效的排序数据范围很大的数组。

基数排序(Radix Sort)

基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。

1. 算法描述

  1. 取得数组中的最大数,并取得位数;
  2. arr为原始数组,从最低位开始取每个位组成radix数组;
  3. 对radix进行计数排序(利用计数排序适用于小范围数的特点);

2. 算法性能

  • 分类 ------------- 内部非比较排序
  • 数据结构 ---------- 数组
  • 最差时间复杂度 ---- O(n * dn)
  • 最优时间复杂度 ---- O(n * dn)
  • 平均时间复杂度 ---- O(n * dn)
  • 所需辅助空间 ------ O(n * dn)
  • 稳定性 ----------- 稳定

3. 动图展示
在这里插入图片描述

4. 代码实现

public static int[] RadixSort(int[] array) {
    if (array == null || array.length < 2) return array;
    // 1.先算出最大数的位数;
    int max = array[0];
    for (int i = 1; i < array.length; i++) {
        max = Math.max(max, array[i]);
    }
    int maxDigit = 0;
    while (max != 0) {
        max /= 10;
        maxDigit++;
    }
    int mod = 10, div = 1;
    // 初始化每位的ArrayList<Integer>
    ArrayList<ArrayList<Integer>> bucketList = new ArrayList<>();
    for (int i = 0; i < 10; i++) bucketList.add(new ArrayList<>());
    // 从最低位开始取每个位组成bucketList数组
    for (int i = 0; i < maxDigit; i++, mod *= 10, div *= 10) {
        for (int j = 0; j < array.length; j++) {
            int num = (array[j] % mod) / div;
            bucketList.get(num).add(array[j]);
        }
        int index = 0;
        // bucketList数组遍历赋值给原数组array
        for (int j = 0; j < bucketList.size(); j++) {
            for (int k = 0; k < bucketList.get(j).size(); k++)
                array[index++] = bucketList.get(j).get(k);
            bucketList.get(j).clear();
        }
    }
    return array;
}

基数排序的时间复杂度是O(n * dn),其中n是待排序元素个数,dn是数字位数。这个时间复杂度不一定优于O(n log n),dn的大小取决于数字位的选择(比如比特位数),和待排序数据所属数据类型的全集的大小;dn决定了进行多少轮处理,而n是每轮处理的操作数目。

如果考虑和比较排序进行对照,基数排序的形式复杂度虽然不一定更小,但由于不进行比较,因此其基本操作的代价较小,而且如果适当的选择基数,dn一般不大于log n,所以基数排序一般要快过基于比较的排序,比如快速排序。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序并不是只能用于整数排序。

桶排序(Bucket Sort)

桶排序也叫箱排序。工作的原理是将数组元素映射到有限数量个桶里,利用计数排序可以定位桶的边界,每个桶再各自进行桶内排序(使用其它排序算法或以递归方式继续使用桶排序)。

1. 算法描述

  1. 设置一个定量的数组当作空桶;
  2. 遍历输入数据,并且把数据一个一个放到对应的桶里去;
  3. 对每个不是空的桶进行排序;
  4. 从不是空的桶里把排好序的数据拼接起来。

注意,如果递归使用桶排序为各个桶排序,则当桶数量为1时要手动减小BucketSize增加下一循环桶的数量,否则会陷入死循环,导致内存溢出

2. 算法性能

  • 分类 ------------- 内部非比较排序
  • 数据结构 --------- 数组
  • 最差时间复杂度 ---- O(nlogn)或O(n^2),只有一个桶,取决于桶内排序方式
  • 最优时间复杂度 ---- O(n),每个元素占一个桶
  • 平均时间复杂度 ---- O(n),保证各个桶内元素个数均匀即可
  • 所需辅助空间 ------ O(n + bn)
  • 稳定性 ----------- 稳定

3. 动图展示

在这里插入图片描述

4. 代码实现

 public static ArrayList<Integer> BucketSort(ArrayList<Integer> array, int bucketSize) {
    if (array == null || array.size() < 2) return array;
    int max = array.get(0), min = array.get(0);
    // 找到最大值最小值
    for (int i = 0; i < array.size(); i++) {
        if (array.get(i) > max)
            max = array.get(i);
        if (array.get(i) < min)
            min = array.get(i);
    }
    int bucketCount = (max - min) / bucketSize + 1;
    ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketCount);
    ArrayList<Integer> resultArr = new ArrayList<>();
    for (int i = 0; i < bucketCount; i++) {
        bucketArr.add(new ArrayList<>());
    }
    for (int i = 0; i < array.size(); i++) {
        bucketArr.get((array.get(i) - min) / bucketSize).add(array.get(i));
    }
    for (int i = 0; i < bucketCount; i++) {
        if (bucketSize == 1) { // 如果带排序数组中有重复数字时
            for (int j = 0; j < bucketArr.get(i).size(); j++)
                resultArr.add(bucketArr.get(i).get(j));
        } else {
            if (bucketCount == 1)
                bucketSize--;
            ArrayList<Integer> temp = BucketSort(bucketArr.get(i), bucketSize);
            for (int j = 0; j < temp.size(); j++)
                resultArr.add(temp.get(j));
        }
    }
    return resultArr;
}

桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。

总结

常见的快速排序、归并排序、堆排序、冒泡排序等属于比较排序。在排序的最终结果里,元素之间的次序依赖于它们之间的比较。每个数都必须和其他数进行比较,才能确定自己的位置。

在冒泡排序之类的排序中,问题规模为n,又因为需要比较n次,所以平均时间复杂度为O(n²)。在归并排序、快速排序之类的排序中,问题规模通过分治法消减为logN次,所以时间复杂度平均O(nlogn)。

比较排序的优势是,适用于各种规模的数据,也不在乎数据的分布,都能进行排序。可以说,比较排序适用于一切需要排序的情况。

计数排序、基数排序、桶排序则属于非比较排序。非比较排序是通过确定每个元素之前,应该有多少个元素来排序。针对数组arr,计算arr[i]之前有多少个元素,则唯一确定了arr[i]在排序后数组中的位置。

非比较排序只要确定每个元素之前的已有的元素个数即可,所有一次遍历即可解决。算法时间复杂度O(n)。

非比较排序时间复杂度底,但由于非比较排序需要占用空间来确定唯一位置。所以对数据规模和数据分布有一定的要求。

以上就是本人对常用经典的十大排序算法的理解,有不足之处,望指点!万分感谢!
在这里插入图片描述

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值